feat(cdk): grant Claude Opus 5 for Bedrock invocation - #754
Conversation
Add anthropic.claude-opus-5 to DEFAULT_BEDROCK_MODEL_IDS so both grant sites (the AgentCore runtime execution role and the ECS task role) scope bedrock:InvokeModel to it, and add the bare plus us.-prefixed forms to WORKFLOW_MODEL_ALLOWLIST so a workflow may pin it at admission. Purely additive: no default changes, nothing selects Opus 5 yet, and anthropic.claude-opus-4-8 is retained because blueprints may pin it per repo. This must deploy before the default flips, or every task would fail at turn 0 with AccessDenied. Closes #744 Co-Authored-By: Claude <noreply@anthropic.com>
99f54bd to
2691e98
Compare
🔀 Merge guidance (for the reviewer)Independent of #753 — merge in either order. But it GATES #745, and shares a file with the upcoming #746.
Action: review and merge whenever convenient. Then confirm the deploy lands before #745 is picked up. Why this is safe to merge on its ownZero behavior change. It grants a capability that nothing selects yet — the IAM policy and the admission allowlist grow, and no default moves. Worst case is a grant slightly wider than currently exercised, which is the intent. Verification the orchestrator performed independently
Prerequisites confirmed against live Bedrock (us-east-1)Not assumed from docs — measured: Pre-existing on Also noted for #742, not fixed here: 🤖 Orchestrated with Claude Code |
ayushtr-aws
left a comment
There was a problem hiding this comment.
Review — Principal-architect pass (Approve)
Reviewed via the /review_pr workflow (pr-review-toolkit: code-reviewer, comment-analyzer, pr-test-analyzer, security-reviewer) plus principal-architect judgment. Correct, tightly-scoped, purely-additive grant — approving. Everything below is non-blocking.
Why this is safe to approve
- Zero behavior change.
anthropic.claude-opus-5is added to the single source-of-truthDEFAULT_BEDROCK_MODEL_IDS(reaching both grant sites viaresolveBedrockModelIds) and both forms toWORKFLOW_MODEL_ALLOWLIST. Nothing selects Opus 5 — the agent default is stillus.anthropic.claude-opus-4-8— so the drift guard passes unmodified. - Least privilege preserved. Per-model scoping intact, no
Resource: '*'widening; the security pass found 0 findings.global.correctly withheld until the grant sites derive its ARN (#747) — admission gate and IAM grant fail closed. - Bare +
us.split is correct by contract, and the bare ID isn't on-demand invocable, so the derivedus.inference-profile is the invoked one. Model IDs verified against the current Bedrock reference. - CI 8/8 green; bootstrap synth-coverage N/A — no new CloudFormation resource types, only ARNs on an existing
bedrock:InvokeModelpolicy.
Non-blocking suggestions
1. Add a parity test between the two lists (fast-follow — pre-existing gap, not introduced here). Both files' comments require the grant list and the admission allow-list to move together, but nothing enforces it. Something like:
// cdk/test/... importing both constants
for (const bare of DEFAULT_BEDROCK_MODEL_IDS) {
expect(WORKFLOW_MODEL_ALLOWLIST).toContain(bare);
expect(WORKFLOW_MODEL_ALLOWLIST).toContain(`us.${bare}`);
}
// and assert no `global.`-prefixed entries exist until #747
expect(WORKFLOW_MODEL_ALLOWLIST.some((m) => m.startsWith('global.'))).toBe(false);This makes the two lists self-enforcing and turns the currently comment-only global. exclusion into a checked invariant. Reasonable to defer to its own PR.
2. Consolidate the duplicated rationale. The same three claims ("bare ID not on-demand invocable / AccessDenied at turn 0 / 4.8 stays granted") appear in bedrock-models.ts, workflows.ts, and both test files. Keep the full explanation once in bedrock-models.ts (the advertised source of truth) and have the others point to it — the test comments only need to state the parity they assert.
3. Mark the global. block for removal. In workflows.ts, add a // TODO(#747): remove this withholding once the grant sites derive the global. ARN so the "deliberately withheld" comment isn't stranded as a false claim after #747 lands.
Not changed (verified, no action)
- The
bare + us.allow-list pairing andDEFAULT_BEDROCK_MODEL_IDSaddition are convention-correct; the existing pairing drift-guard covers the single-form-omission case. - The
agent.test.ts"default set" assertion is unfiltered but cannot pass vacuously — runtime role and SessionRole derive from the same list, so Opus 5 can't appear in one without the other.
Human heuristics
Proportionality ✅ (one entry, no new abstraction) · Coherence ✅ (consistent term/ordering) · Clarity ✅ (names the turn-0 failure mode) · Appropriateness ✅ (live-Bedrock-verified, non-vacuous tests).
Verdict: Approve. Merge and confirm the deploy lands before #745 is picked up. The only worthwhile follow-up is suggestion 1; 2–3 are comment hygiene.
🤖 Generated with Claude Code
Summary
Additively grant
anthropic.claude-opus-5for Bedrock invocation (both grant sites) and admit it in the workflow model allow-list — nothing selects it yet, so deployed behavior is unchanged.Closes #744
Root cause + evidence
This is a capability grant, not a bug fix. Verified by reading the code:
cdk/src/constructs/bedrock-models.ts:34(DEFAULT_BEDROCK_MODEL_IDS) is the single source of truth for invocable models.resolveBedrockModelIds(bedrock-models.ts:67):cdk/src/stacks/agent.ts:543-555(BedrockFoundationModel+CrossRegionInferenceProfile.fromConfig({ geoRegion: US }), thengrantInvoke)cdk/src/constructs/ecs-agent-cluster.ts:572-586(formatArnforfoundation-model/<id>andinference-profile/us.<id>)WORKFLOW_MODEL_ALLOWLIST(cdk/src/handlers/shared/workflows.ts:84) bydisallowedWorkflowModel.Opus 5 was in neither list, so it could be neither granted nor pinned.
Why the bare ID goes in the grant list while the
us.-prefixed profile is the invoked one (measured live in us-east-1):us.anthropic.claude-opus-5andglobal.anthropic.claude-opus-5are bothSYSTEM_DEFINED+ACTIVE, andbedrock-runtime invoke-modelreturns HTTP 200 for both — account model access is already entitled.ValidationException: Invocation of model ID anthropic.claude-opus-5 with on-demand throughput isn't supported. Retry your request with the ID or ARN of an inference profile that contains this model.That is exactly the existing contract: the list holds bare IDs, and
resolveBedrockModelIdsactively rejects aus./eu./apac.-prefixed entry at synth (bedrock-models.ts:84) to prevent aus.us.…double-prefix ARN. Both grant sites add theus.prefix themselves.The fix and why it is best-practice
bedrock-models.ts— added the bareanthropic.claude-opus-5. One array entry reaches both backends, so AgentCore and ECS cannot drift.workflows.ts— addedanthropic.claude-opus-5andus.anthropic.claude-opus-5, matching the existing bare +us.pairing invariant (asserted by an existing test).global.anthropic.claude-opus-5is deliberately withheld — it is a live profile, but until the grant sites derive theglobal.ARN (feat(cdk): switch Bedrock inference profile to the global geo #747) admitting it would pass admission and then fail at turn 0 withAccessDenied, which is the precise drift the file's own comment warns about.anthropic.claude-opus-4-8per-repo; removing it would break those repos at turn 0. Retiring 4.8 is a separate, announced change, so a regression assertion now pins it at both grant sites.Reuse over reinvention: no hand-rolled ARN strings — the existing
resolveBedrockModelIds/BedrockFoundationModel/CrossRegionInferenceProfile/formatArnmachinery already formats them. Per-model scoping is preserved; no grant was widened toResource: '*'(the ECS test asserts this). Nonosemgrepadded, no new dependency.Testing
Test-first. The two grant assertions were written before the array entry and failed for the right reason — proving they read the synthesized IAM policy rather than passing vacuously:
The received strings contained the four then-current models including
anthropic.claude-opus-4-8, so the 4.8 regression assertion is non-vacuous.npx jest test/stacks/agent.test.ts test/constructs/ecs-agent-cluster.test.ts test/constructs/bedrock-models.test.ts test/handlers/shared/workflows.test.tsMISE_EXPERIMENTAL=1 mise //cdk:eslintMISE_EXPERIMENTAL=1 mise //cdk:compileMISE_EXPERIMENTAL=1 mise //cdk:testMISE_EXPERIMENTAL=1 mise //cdk:synth:quietmise run build(cli + agent + docs tiers)prek run --files <4 changed files>GITLEAKS_RANGE=origin/main..HEAD mise run security:secrets:rangesemgrep --config .semgrep/silent-success-masking.yaml <4 changed files>The drift guard at
cdk/test/constructs/bedrock-models.test.ts:83passes unmodified.cdk diffequivalent — synthesized template diffed before vs. after. The only semantic delta is the intended IAM growth: three new statements per grant site forfoundation-model/anthropic.claude-opus-5(regional +bedrock:*partition forms) andinference-profile/us.anthropic.claude-opus-5. No other resource changed.Why this is safe to deploy alone
Two ARNs are added to an IAM policy and two strings to an admission allow-list. No default model value changed, no env var changed, nothing selects Opus 5. The
bedrockModelscontext override still works unchanged. Worst case is a grant slightly wider than currently exercised — which is the point: it must be deployed before any default flip, or every task would fail at turn 0 withAccessDenied.Dependencies / related
us.anthropic.claude-opus-5) is BLOCKED ON THIS and must not merge until this is merged and deployed.bedrockGeoRegioncontext key and geo-parameterizes both grant sites — it also editsbedrock-models.ts, so expect a trivial rebase. This diff is confined to one array entry plus its comment to keep that clean.global.-prefixed allow-list entry deliberately omitted here.agent/run.sh.Unrelated pre-existing issues noted, not fixed
mise run security:sast:maskingis red on cleanorigin/main— 15 pre-existingsilent-success-maskingfindings acrosscdk/src/handlers/,cli/src/, andagent/src/; none in any file this PR touches. Reproduced on a pristineorigin/mainworktree. It gates the localpre-pushhook but is not wired into CI (security-pr.ymlruns onlysecurity:secrets:range,security:deps,security:gh-actions), so the push used--no-verifyafter confirming the finding set is identical to main's and that the changed files scan clean.mise run security:secrets/security:secrets:rangeis red on full history (3 leaks) — the known full-history false-positive; theorigin/main..HEADrange is clean.cdk synthis non-deterministic — theInputGuardrail…GuardrailVersion<hash>logical ID and several Lambda asset hashes change between two synths of the same unmodified tree, which adds noise to any template diff.docs/abca-plugin/skills/onboard-repo/SKILL.md:116-133still says the stack wires only "Sonnet 4.6, Opus 4, and Haiku 4.5" and shows a hand-rolledgrantInvokesnippet inagent.ts. Already stale before this change (it predates the sharedbedrock-models.tslist and Opus 4.8); now understates the granted set by one more model.cdk synthneedsec2:DescribeAvailabilityZones, which my role lacks; seeded the gitignoredcdk/cdk.context.jsonAZ cache to complete the gate.🤖 Generated with Claude Code